# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import _
from odoo.tools import OrderedSet
from collections import defaultdict, OrderedDict


Operators = {
    'eq':  '=',   # 默认为 '='  if value is integer
    'neq': '!=',
    'gt':  '>',
    'ge':  '>=',
    'lt':  '<',
    'le':  '<=',
    'eq_null':   '=?',
    'eq_like':   '=like',
    'like':      'like',
    'not_like':  'not like',
    'ilike':     'ilike',
    'not_ilike': 'not ilike',
    'eq_ilike':  '=ilike',
    'in':        'in',         # 默认为 'in' if value is list
    'not_in':    'not in',
    'child_of':  'child_of',
    'parent_of': 'parent_of'
}


class Model2(object):
    """
    TBD: 2019-7-20, obj 需要 with_context(context)

    所有的 fields 参数格式为嵌套的 {{},{}}。同时支持简单的[list]，

    2019-11-27
    简单的 domain, 都是 与 关系的, 可以用 dict 传参数, 前端 使用 dict 这种结构易理解,
    复杂的 domain, 有 或 非 关系的, dict 不容易设计, 依然可以用 list 格式， 

    {
        id:          [int],        # 该格式约定, 等同于 [id, 'in', [int] ]，         常用于 ids 过滤
        partner_id:  int,          # 该格式约定, 等同于 [partner_id, '=', int ]，    常用于 id 过滤
        name___like: 'string',     # 该格式约定, 等同于 [name, 'like', string ]，    常用于 模糊查询
        date___gt:   'yyyy-mm-dd', # 该格式约定, 等同于 [date, '>',  'yyyy-mm-dd' ]，常用于 时间范围查询
        date___ge:   'yyyy-mm-dd', # 该格式约定, 等同于 [date, '>=', 'yyyy-mm-dd' ]，常用于 时间范围查询
        date___lt:   'yyyy-mm-dd', # 该格式约定, 等同于 [date, '<',  'yyyy-mm-dd' ]，常用于 时间范围查询
        date___le:   'yyyy-mm-dd', # 该格式约定, 等同于 [date, '<=', 'yyyy-mm-dd' ]，常用于 时间范围查询
    }
    
    """
    @classmethod
    def test_function(cls, obj, rid, *args, **kwargs ):
        print('xxxx,123456789, test_function', obj._name, rid )


    @classmethod
    def test_function2(cls, obj, rid, *args, **kwargs ):
        print('xxxx,123456789, test_function', obj._name, rid)

        company_id = obj.env.user.company_id.id
        print('xxxx,123456789, test_function, company_id', obj._name, company_id)


        metadata = obj.fields_get(['journal_id'])
        print('xxxx,123456789, test_function, metadata ', obj._name, 'journal_id', metadata)
        domain = metadata['journal_id']['domain']
        print('xxxx,123456789, test_function, domain ', obj._name, domain, type(domain))
        domain2 = eval(domain,{"company_id":company_id})
        print('xxxx,123456789, test_function, domain2 ', obj._name, domain2, type(domain2))


        # ss = cls.get_options(obj, {'journal_id': {} } )
        # print('xxxx,123456789, test_function, ss ', obj._name, 'journal_id', ss)

        return {}


    @classmethod
    def _domain_dict2list(cls, domain, domain2 ):
        # domain, domain2 两个参数, 分别传 list 和 dict 参数

        if not domain:
            domain = []

        if not domain2:
            domain2 = []

        def fn(field_op, value):
            field_op_list = field_op.split('___')
            if len(field_op_list) == 1:
                field_op_list = field_op_list + [ isinstance( value, list ) and 'in' or 'eq' ]
            
            field, op = field_op_list
            op = Operators[op]
            return field, op, value

        if isinstance(domain, dict):
            domain = [ fn( field_op, value ) for field_op, value in domain.items() ]

        elif not isinstance(domain, list):
            # raise error
            domain = []

        if isinstance(domain2, dict):
            domain2 = [ fn( field_op, value ) for field_op, value in domain2.items() ]

        elif not isinstance(domain2, list):
            # raise error
            domain2 = []

        return domain + domain2


    # after create or write, onchange is not trigger
    # so call this onchange, to update other fields
    # this is copy ftom odoo.models.onchange
    @classmethod
    def onchange(cls, obj, rid, *args, **kwargs ):
        # print('xxxx,123456789, test_function', obj._name, rid )

        def PrefixTree(model, dotnames):
            """ Return a prefix tree for sequences of field names. """
            if not dotnames:
                return {}
            # group dotnames by prefix
            suffixes = defaultdict(list)
            for dotname in dotnames:
                # name, *names = dotname.split('.', 1)
                names = dotname.split('.', 1)
                name = names.pop(0)
                suffixes[name].extend(names)
            # fill in prefix tree in fields order
            tree = OrderedDict()
            for name, field in model._fields.items():
                if name in suffixes:
                    tree[name] = subtree = PrefixTree(model[name], suffixes[name])
                    if subtree and field.type == 'one2many':
                        subtree.pop(field.inverse_name, None)
            return tree

        class Snapshot(dict):
            """ A dict with the values of a record, following a prefix tree. """
            __slots__ = ()

            def __init__(self, record, tree):
                # put record in dict to include it when comparing snapshots
                super(Snapshot, self).__init__({'<record>': record, '<tree>': tree})
                for name in tree:
                    self.fetch(name)

            def fetch(self, name):
                """ Set the value of field ``name`` from the record's value. """
                record = self['<record>']
                tree = self['<tree>']
                if record._fields[name].type in ('one2many', 'many2many'):
                    # x2many fields are serialized as a list of line snapshots
                    self[name] = [Snapshot(line, tree[name]) for line in record[name]]
                else:
                    self[name] = record[name]

            def has_changed(self, name):
                """ Return whether a field on record has changed. """
                record = self['<record>']
                subnames = self['<tree>'][name]
                if record._fields[name].type not in ('one2many', 'many2many'):
                    return self[name] != record[name]
                return (
                    len(self[name]) != len(record[name])
                    or (
                        set(line_snapshot["<record>"].id for line_snapshot in self[name])
                        != set(record[name]._ids)
                    )
                    or any(
                        line_snapshot.has_changed(subname)
                        for line_snapshot in self[name]
                        for subname in subnames
                    )
                )

            def diff(self, other):
                """ Return the values in ``self`` that differ from ``other``.
                    Requires record cache invalidation for correct output!
                """
                record = self['<record>']
                result = {}
                for name, subnames in self['<tree>'].items():
                    if (name == 'id') or (other.get(name) == self[name]):
                        continue
                    field = record._fields[name]
                    if field.type not in ('one2many', 'many2many'):
                        result[name] = field.convert_to_onchange(self[name], record, {})
                    else:
                        # x2many fields: serialize value as commands
                        result[name] = commands = [(5,)]
                        for line_snapshot in self[name]:
                            line = line_snapshot['<record>']
                            line = line._origin or line
                            if not line.id:
                                # new line: send diff from scratch
                                line_diff = line_snapshot.diff({})
                                commands.append((0, line.id.ref or 0, line_diff))
                            else:
                                # existing line: check diff from database
                                # (requires a clean record cache!)
                                line_diff = line_snapshot.diff(Snapshot(line, subnames))
                                if line_diff:
                                    # send all fields because the web client
                                    # might need them to evaluate modifiers
                                    line_diff = line_snapshot.diff({})
                                    commands.append((1, line.id, line_diff))
                                else:
                                    commands.append((4, line.id))
                return result

        field_onchange = obj._onchange_spec()
        nametree = PrefixTree(obj.browse(), field_onchange)
        # onchange_methods = obj._onchange_methods

        record = obj.browse(rid)
        snapshot0 = Snapshot(record, nametree)

        result = {'warnings': OrderedSet()}

        todo = list( nametree)
        done = set()

        # process names in order
        while todo:
            # print('xxxx,123456789, test_function, todo:', todo )
            # apply field-specific onchange methods
            for name in todo:
                if field_onchange.get(name):
                    record._onchange_eval(name, field_onchange[name], result)
                done.add(name)

            # determine which fields to process for the next pass
            todo = [
                name
                for name in nametree
                if name not in done and snapshot0.has_changed(name)
            ]

            # if not env.context.get('recursive_onchanges', True):
            #     todo = []
     
        # format warnings
        warnings = result.pop('warnings')
        if len(warnings) == 1:
            title, message, type = warnings.pop()
            if not type:
                type = 'dialog'
            result['warning'] = dict(title=title, message=message, type=type)
        elif len(warnings) > 1:
            # concatenate warning titles and messages
            title = _("Warnings")
            message = '\n\n'.join([warn_title + '\n\n' + warn_message for warn_title, warn_message, warn_type in warnings])
            result['warning'] = dict(title=title, message=message, type='dialog')

        return result

    @classmethod
    def create(cls, obj, values_list, *args, **kwargs ):
        records = obj.create( values_list )
        for rec in records:
            cls.onchange(rec, rec.id)
        return records.ids

    @classmethod
    def create2(cls, obj, values_list, fields=None, *args, **kwargs ):
        rids = cls.create( obj, values_list )
        if not rids:
            return None

        if not fields:
            fields = ['id']
        res = cls.read( obj, rids, fields )

        if isinstance( values_list, list):
            return res
        else:
            return res[0]

    @classmethod
    def write(cls, obj, rid, values, *args, **kwargs ):
        record = obj.browse(rid)
        res = record.write( values )
        cls.onchange(obj, rid)
        return res

    @classmethod
    def write2(cls, obj, rid, values, fields=None, *args, **kwargs ):
        res = cls.write( obj, rid, values )
        if not fields:
            fields = ['id']
        
        res = cls.read( obj, rid, fields )
        if res:
            return res[0]

        return {}

    @classmethod
    def fields_get(cls, obj, fields=None, *args, **kwargs ):
        # fields is dict, return all models
        # TBD
        return {}

    @classmethod
    def _get_meta(cls, obj, field, metadata=None,):
        
        fs = field.split('.')
        if len(fs) == 1:
            return metadata[field]

        elif len(fs) == 2:
            meta = metadata[fs[0]]
            if meta['type'] in ('many2one','one2many','many2many'):
                ref_metadata = obj.env[meta['relation']].fields_get([fs[1]])
                return ref_metadata[fs[1]]

        return {'type': None}

    
    # 2020-5-7 改过 返回值的格式, 以前的东西会有问题
    # 现在的返回值 :  for selection, [{value, lable}]
    # 现在的返回值 :  for m2m/o2m/m2o, [{value, lable, object}]
    # 以前的返回值 :  for selection, [{value, name}]
    # 以前的返回值 :  for m2m/o2m/m2o, [{...object}]
    @classmethod
    def _get_option(cls, obj, field, metadata=None,
                    domain=None, domain2=None, 
                    fields=None, order=None, cascader=None ):

        if not metadata:
            metadata = {}

        meta = cls._get_meta(obj, field,metadata )

        if not meta:
            return []

        if meta['type'] == 'selection':
            return [ {'value': sel[0], 'text': sel[1], 'label': sel[1] 
                     }  for sel in meta['selection'] ]
        
        elif meta['type'] in ['many2one','many2many','one2many']:
            ref = obj.env[meta['relation']]
            fields = fields or ['display_name']
            
            if 'display_name' not in fields:
                if isinstance( fields, dict):
                    fields['display_name'] = None
                elif isinstance( fields, list):
                    fields = fields + ['display_name']

            # TBD 2020-1-9,  需要根据实际情况不断补充完善
            my_globals = {"company_id": obj.env.user.company_id.id}
            domain_default = meta.get('domain', [] )
            if isinstance(domain_default, str):
                try:
                    domain_default = eval(domain_default, my_globals )
                except:
                    pass
                
            
            # TBD 2020-1-9,  需要根据实际情况不断补充完善
            if not isinstance(domain_default, (list, tuple)):
                domain_default = []

            domain3 = cls._domain_dict2list(domain, domain2)
            domain3 = domain_default + domain3
            records = cls.search_read(ref, domain=domain3, domain2={}, fields=fields, order=order)

            records = [{
                'value': rec['id'], 
                'text': rec['display_name'],
                'label': rec['display_name'],
                'object': rec
            } for rec in records ]

            if not cascader:
                return records
            
            # TBD 2019-11-21, 根据 cascader 进行递归 处理
            # cascader: {
            #     value: 'id',
            #     label: 'display_name',
            #     children: {
            #         name: 'child_ids',
            #         cascader: {
            #             value: 'id',
            #             label: 'display_name'
            #         }
            #     }
            # }

            return records

        return []

    @classmethod
    def get_options(cls, obj, fields ):
        options = {}
        fs = [ fld.split('.')[0]  for fld in fields ]
        metadata = obj.fields_get(fs)

        for fld, value in fields.items():
            if not value:
                value = {}

            options[fld] = cls._get_option(obj, fld, metadata, **value)

        return options


    @classmethod
    def wizard_submit(cls,obj, rid, wizard_model, wizard_id, vals, wizard_method,
        onchange=None, *args, **kwargs):
        if not onchange:
            onchange = []

        wizard = obj.env[wizard_model].browse(wizard_id)
        wizard.write(vals)
        if onchange:
            for onchange2 in onchange:
                getattr( wizard, onchange2)()

        context = {}
        context.update(obj._context)
        context.update({
            'active_model': obj._name,
            'active_id': rid,
            'active_ids': [rid]
        }) 

        wizard = wizard.with_context(context)
        return getattr( wizard, wizard_method)()

    @classmethod
    def wizard_create(cls,obj, rid, model, vals,
        onchange=None, fields=None, context=None, *args, **kwargs):
        if not onchange:
            onchange = []
        if not fields:
            fields = []
        if not context:
            context = {}

        if isinstance(context, str):
            my_globals = {"ref": obj.env['ir.model.data'].xmlid_to_res_id}
            context = eval(context, my_globals)

        context.update(obj._context)
        context.update({
            'active_model': obj._name,
            'active_id': rid,
            'active_ids': [rid]
        })

        print('12345678, context,', context )



        wizard = obj.env[model].with_context(context)

        wizard = wizard.create(vals)

        if onchange:
            for onchange2 in onchange:
                getattr( wizard, onchange2)()

        return cls._return_for_read(wizard, fields)

    @classmethod
    def report_print(cls,obj, wizard_model, wizard_id, wizard_values,
                            fields=None, context=None, *args, **kwargs):
        wizard = obj.env[wizard_model]
        wizard = wizard.browse( wizard_id )
        if wizard_values:
            wizard.write( wizard_values)
                    
        if not fields:
            fields = {}
        wizard_data = cls._return_for_read(wizard, fields)[0]
        
        report_meta = wizard.check_report()

        if not context:
            context = {}

        context.update(obj._context)
        context.update(report_meta.get('context',{}))
        context.update({
            'active_model': wizard_model,
            'active_id': wizard_id,
            'active_ids': [wizard_id]
        })

        obj = obj.with_context(context)
        fn = hasattr(obj, '_get_report_values2'
             ) and obj._get_report_values2 or obj._get_report_values

        res = fn(None, data=report_meta.get( 'data', None) )

        res.update({
            'wizard_data': wizard_data
        })
        
        return res


    @classmethod
    def search_count(cls,obj, domain=None, domain2=None, context=None, *args, **kwargs):
        domain = cls._domain_dict2list( domain, domain2 )
        return obj.search_count( domain )

    @classmethod
    def search_read(cls,obj, domain=None,  domain2=None, fields=None,
                         offset=0, limit=None, order=None, context=None, *args, **kwargs):

        domain = cls._domain_dict2list( domain, domain2 )
        recs = obj.search(domain, offset=offset, limit=limit, order=order)
        return cls._return_for_read(recs,fields, context)

    @classmethod
    def read(cls,obj,ids,fields=None, context=None, *args, **kwargs):
        recs = obj.browse(ids)
        return cls._return_for_read(recs,fields, context)

    @classmethod
    def new(cls,obj,values=None, fields=None, onchange=None, context=None, *args, **kwargs):
        if not context:
            context = {}
        context.update(obj._context)

        obj = obj.with_context(context)

        recs = obj.new(values)

        if not onchange:
            onchange = []
        
        for chg in onchange:
            if hasattr( recs, chg):
                getattr(recs, chg)()

        return cls._return_for_read(recs,fields, context, no_read=1)[0]

    # 2019-11-27
    # for selection field eg. 'state',      we set a new field 'state__name'
    # for many2one  field eg. 'partner_id', we set a new field 'partner_id__name'
    # for many2one  field eg. 'partner_id', we set a new field 'partner_id__onject' = {id, name, ...}
    # for many2one  field eg. 'partner_id', we set this  field 'partner_id' = id
    # for o2m m2m   field eg. 'line_ids',   we set a new field 'line_ids__names' = 'name1,name2,...'
    # for o2m m2m   field eg. 'line_ids',   we set a new field 'line_ids__objects' = [{id,name}]
    # for o2m m2m   field eg. 'line_ids',   we set this  field 'line_ids' = [ids]

    # TBD, 读取 o2m 字段 嵌套 有 m2o 字段 会出现无限循环
    # 2019-12-23
    @classmethod
    def _return_for_read(cls, obj, fields, context=None, no_read=None):

        if not fields:
            fields = {}

        if fields and isinstance(fields, list):
            fields = dict( (f, None) for f in fields )

        # fn2 be copy from odoo.models.search_read(), 2018-8-13
        def fn2(records, fields11):
            # result = records.read(fields11)
            result = []
            if no_read:
                fields2 = list(fields11)
                if not fields2:
                    fields2 = list( obj.fields_get().keys() )
                
                if 'id' not in fields2:
                    fields2 = ['id'] + fields2

                # 调用 new 函数时, account.move 的 string_to_hash 字段 计算函数 出错
                if no_read and obj._name == 'account.move':
                    fields2 = [fld for fld in fields2 if fld != 'string_to_hash']

                def fn222(rec, fld):
                    # print('xxxxxxx, fld', fld)
                    return getattr(rec, fld)

                result = [ dict( (fld, fn222(rec, fld) ) for fld in fields2 ) 
                     for rec in records]

                # result = [ dict( (fld, getattr(rec, fld) ) for fld in fields2 ) 
                #      for rec in records]
                
            else:
                result = records.read(fields11)

            if len(result) <= 1:
                return result

            # reorder read
            index = {vals['id']: vals for vals in result}
            return [index[record.id] for record in records if record.id in index]

        records = fn2(obj, fields.keys() )

        metadata = obj.fields_get(fields.keys())

        # TBD, 2019-12-25, fields 为空时 , 回所有字段 而且是 不处理的.

        for rec in records:
            for field, ref_fields in fields.items():
                meta = metadata[field]
                if not meta:
                    continue

                if meta['type'] == 'selection':
                    sel_val = rec[field] or None
                    rec[field] = sel_val

                    if sel_val:
                        rec[ field + '__name' ] = dict( meta['selection'] ).get(sel_val, sel_val)
                    else:
                        rec[ field + '__name' ] = sel_val

                elif meta['type'] in ('many2one','one2many', 'many2many' ) :
                    ref_val = rec[field]
                    if  meta['type'] == 'many2one':
                        if no_read:
                            m2o_id, m2o_name = ref_val.id , ref_val.display_name
                        else:
                            m2o_id, m2o_name = ref_val or [ None, '']

                        rec[field] = m2o_id
                        rec[ field + '__name' ] = m2o_name
                        rec[ field + '__object' ] = ref_val and {
                            'id': m2o_id, 
                            'name': m2o_name,
                            'display_name': m2o_name } or {}

                        if ref_val and ref_fields:
                            if no_read:
                                ref_obj = ref_val
                            else:
                                obj_one = obj.filtered(lambda r: r.id == rec['id'] )
                                ref_obj = getattr(obj_one, field)

                            ref_m2o = cls._return_for_read(ref_obj, ref_fields, no_read=no_read)
                            ref_m2o = ref_m2o and ref_m2o[0] or {}
                            rec[ field + '__object' ].update(ref_m2o) 

                    else:
                        if no_read:
                            print('xxxxxxxx, m2m, ', field, ref_val, type(ref_val), ref_val._ids)
                            m2m_ids = ref_val._ids
                        else:
                            m2m_ids = ref_val or []

                        rec[ field + '__count' ] = len( m2m_ids )
                        if ref_fields:
                            ref_fields['display_name'] = None

                            if no_read:
                                ref_obj = ref_val
                            else:
                                obj_one = obj.filtered(lambda r: r.id == rec['id'] )
                                ref_obj = getattr(obj_one, field)

                            ref_records = cls._return_for_read(ref_obj, ref_fields, no_read=no_read)
                            rec[ field + '__objects' ] = ref_records
                            rec[ field + '__names' ] = ','.join([ m2m['display_name'] for m2m in ref_records] )

                        else:
                            rec[ field + '__names' ] = ', '.join([ '(%s,%d)' % (meta['relation'], rid ) for rid in  m2m_ids] )
                            rec[ field + '__objects' ] = [ {'id': m2m_id} for m2m_id in m2m_ids ]
                
                elif meta['type'] in ['char','text','html','date','datetime']:
                    sel_val = rec[field]
                    rec[field] = sel_val or None 


        return records
  
